package code.google.jcontainer;

import code.google.jcontainer.util.ClassUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.log4j.Logger;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Store component metadata
 *
 * @author <a href="mailto:jfox.young@gmail.com">Young Yang</a>
 */
public class ComponentMeta {

    private final static Logger logger = Logger.getLogger(ComponentMeta.class);

    /**
     * the annotation annotated on component class to declare component type
     */
    private Annotation componentAnnotation;

    /**
     * component name specified in annotation
     */
    private String name;

    /**
     *  all interfaces this component implements, used to create dynamic proxy instance
     */
    private Class<?>[] interfaceClasses;

    /**
     * Component class
     */
    private Class<?> componentClass;

    /**
     * Component instance
     */
    private Object componentObject;

    /**
     * class annotations, component type Annotation is excluded
     */
    private List<Annotation> classAnnotations = new ArrayList<Annotation>(3);

    /**
     * all field annotations
     */
    private Map<Field, Annotation[]> fieldAnnotations = new HashMap<Field, Annotation[]>();

    /**
     * all method annotations 
     */
    private Map<Method, Annotation[]> methodAnnotations = new HashMap<Method, Annotation[]>();

    private static Log log = LogFactory.getLog(ComponentMeta.class);

    /**
     * a flag for recoding if this component is first use
     * Some task need to be done after FIRST_GET, for example, \@Inject
     */
    private boolean firstGet = true;

    public ComponentMeta(Annotation componentAnnotation, Class<?> componentClass) {
        this.componentClass = componentClass;
        this.componentAnnotation = componentAnnotation;
        this.interfaceClasses = ClassUtils.getAllInterfaces(componentClass);
        try {
            // invoke name method of annotation to get the component name
            Method nameMethod = componentAnnotation.getClass().getMethod("name");
            if(nameMethod == null) {
                log.warn("Component Annotation " + componentAnnotation + " doesn't have \"String name()\" " + " method, will use component class  " + componentClass.getName() + " as name.");
                this.name = componentClass.getName();
            }
            else {
                String name = (String)nameMethod.invoke(componentAnnotation);
                if (name != null && !name.trim().equals("")) {
                    this.name = name;
                }
            }
        }
        catch (Exception e) {
            logger.info("parse name method of annotation " + componentAnnotation.getClass() + " failed.", e );
            this.name = componentClass.getName();
        }
        introspectClassAnnotations();
        introspectMethods();
        introspectFields();
    }

    /**
     * introspect class annotations
     */
    protected void introspectClassAnnotations(){
        Annotation[] allClassAnnotations = componentClass.getAnnotations();
        for(Annotation classAnnotation : allClassAnnotations) {
            if(!classAnnotation.annotationType().equals(getComponentAnnotation().annotationType())) {
                this.classAnnotations.add(classAnnotation);
            }
        }
    }

    /**
     * introspect public methods
     */
    protected void introspectMethods(){
        for(Method method : componentClass.getMethods()){
            //cache method annotations
            if(method.getAnnotations() != null && method.getAnnotations().length !=0) {
                methodAnnotations.put(method, method.getAnnotations());
            }
        }
    }

    /**
     * Introspect all Fields, includes Fields in super classes
     */
    protected void introspectFields(){
        Field[] allDeclaredFields = ClassUtils.getAllDeclaredFields(componentClass);
        for(Field field : allDeclaredFields){
            Annotation[] annotations =  field.getAnnotations();
            if(annotations != null && annotations.length !=0) {
                fieldAnnotations.put(field, annotations);
            }
        }
    }

    /**
     * component name
     */
    public String getName() {
        return name;
    }

    /**
     * get all interfaces
     */
    public Class<?>[] getInterfaceClasses() {
        return interfaceClasses;
    }

    /**
     * get component class
     */
    public Class<?> getComponentClass() {
        return componentClass;
    }

    /**
     * get Component instance
     */
    public Object getComponentObject() {
        firstGet = false; // set firstGet false then
        return componentObject;
    }

    /**
     * set component instance
     */
    public void setComponentObject(Object componentObject) {
        this.componentObject = componentObject;
    }

    /**
     * get the component annotation
     */
    public Annotation getComponentAnnotation() {
        return componentAnnotation;
    }

    /**
     * Get class annotations
     */
    public Annotation[] getClassAnnotations(){
        return classAnnotations.toArray(new Annotation[classAnnotations.size()]);
    }

    /**
     * Get Field annotation Map
     */
    public Map<Field, Annotation[]> getFieldAnnotations() {
        return Collections.unmodifiableMap(fieldAnnotations);
    }

    /**
     * Get Method annotation Map
     */
    public Map<Method, Annotation[]> getMethodAnnotations() {
        return Collections.unmodifiableMap(methodAnnotations);
    }

    /**
     * determine if the component has served before
     */
    public boolean isFirstGet() {
        return firstGet;
    }

}
